Curator Affiliate Link Creation API

概述 | Overview

该接口用于 Curator(策展人/店主)为自己拥有的 Post 创建推广联盟链接,通过指定 Promoter(推广者)的手机号或邮箱,建立双方的合作关联。


接口信息 | Endpoint Information

属性
请求地址 /promoter-association/curator/affiliate-link
请求方式 POST
认证方式 JWT Bearer Token (Required)
Content-Type application/json

请求头 | Request Headers

Header 类型 必填 说明
Authorization string JWT Bearer Token (Bearer <token>)
Content-Type string 固定值 application/json
from string - 客户端标识 (示例值: client)
timezone string - 时区信息 (示例值: Asia/Shanghai)
x-track-id string - 追踪 ID (用于日志关联)

示例

POST /promoter-association/curator/affiliate-link HTTP/1.1
Host: release.katana-api.1m.app
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
from: client
timezone: Asia/Shanghai
x-track-id: 1540447c-78a1-4d7a-a2c1-a523d659cc20

请求参数 | Request Parameters

Body (JSON)

字段 类型 必填 说明
curatorId string (UUID) Curator 用户 ID(必须与当前认证用户 ID 一致)
postAlias string Post 的 URL 别名(Post 必须属于该 Curator 且允许转售)
phoneNumberOrEmail string Promoter 的手机号或邮箱(需已验证)
note string - 备注(可选,用于记录合作信息)

请求示例

{
  "phoneNumberOrEmail": "neo.wang.ext+20@1m.app",
  "curatorId": "1ee2b015-5390-44ba-a677-8cbd53e8066f",
  "postAlias": "000011",
  "note": "合作备注信息"
}

响应结构 | Response Structure

成功响应 (200 OK)

interface CreateAffiliateLinkResponse {
  // Curator 信息
  curator: {
    id: string;        // Curator 用户 ID
    vanityUrl: string; // Curator 店铺别名 URL
  };

  // Post 信息
  post: {
    id: string;        // Post 数据库 ID
    alias: string;     // Post URL 别名
  };

  // Promoter 信息
  promoter: {
    id: string;              // Promoter 用户 ID
    email: string;           // Promoter 邮箱
    phoneNumber: string;     // Promoter 手机号
    vanityUrl: string;       // Promoter 店铺别名 URL
  };

  // 推广相关
  affiliateCode: string;  // Promoter 推广码
  note?: string;         // 备注(如有)
}

响应示例

{
  "curator": {
    "id": "1ee2b015-5390-44ba-a677-8cbd53e8066f",
    "vanityUrl": "curator-store"
  },
  "post": {
    "id": "post-uuid-12345",
    "alias": "000011"
  },
  "promoter": {
    "id": "promoter-uuid-67890",
    "email": "neo.wang.ext+20@1m.app",
    "phoneNumber": "+1234567890",
    "vanityUrl": "promoter-store"
  },
  "affiliateCode": "AFF123456",
  "note": "合作备注信息"
}

错误响应 | Error Responses

400 Bad Request - 参数验证失败

{
  "statusCode": 400,
  "message": ["phoneNumberOrEmail must be a valid email or phone number"],
  "error": "Bad Request"
}

400 Bad Request - 权限不足

{
  "statusCode": 400,
  "message": "You do not have the permission to perform this operation.",
  "error": "Bad Request"
}

[!warning] 权限说明 curatorId 必须与当前 JWT Token 中的用户 ID 一致,否则返回此错误。

400 Bad Request - 不能为 Post 所有者创建链接

{
  "statusCode": 400,
  "message": "Can not create affiliate-link for the post owner.",
  "error": "Bad Request"
}

400 Bad Request - Promoter 已转售该 Post

{
  "statusCode": 400,
  "message": "The promoter has already resold this post",
  "error": "Bad Request"
}

400 Bad Request - 不能为商品所有者创建链接

{
  "statusCode": 400,
  "message": "Can not create affiliate-link for the product owner",
  "error": "Bad Request"
}

404 Not Found - Curator 或 Post 不存在

{
  "statusCode": 404,
  "message": "Resource not found",
  "error": "Not Found"
}

业务逻辑 | Business Logic

核心流程图

flowchart TD
    A[接收请求] --> B[验证 JWT Token]
    B --> C{curatorId == userId?}
    C -->|否| D[返回 400 权限错误]
    C -->|是| E[验证 phoneNumberOrEmail 格式]
    E -->|无效| F[返回 400 参数错误]
    E -->|有效| G[并行查询 Curator、Post、Promoter]
    G --> H{Curator/Post 存在?}
    H -->|否| I[返回 404]
    H -->|是| J{Promoter == Curator?}
    J -->|是| K[返回 400 不能为所有者创建]
    J -->|否| L{Promoter 已转售 Post?}
    L -->|是| M[返回 400 已转售]
    L -->|否| N{Promoter 是商品所有者?}
    N -->|是| O[返回 400 不能为商品所有者创建]
    N -->|否| P{Promoter 存在?}
    P -->|否| Q[创建新用户<br/>role=CONSUMER<br/>type=GUEST]
    P -->|是| R[使用现有 Promoter]
    Q --> S[创建/更新<br/>PromoterAssociation]
    R --> S
    S --> T[发送事件<br/>AddPostAccessPromoter]
    T --> U[返回响应]

详细处理步骤

0. 权限验证

const userId = this.requestContextService.getUserId();
if (userId !== curatorId) {
  throw new BadRequestException('You do not have the permission to perform this operation.');
}

1. 参数格式验证

使用 class-validator 验证 phoneNumberOrEmail 必须是有效的邮箱或手机号格式。

2. 并行数据查询

const [curator, post, promoter] = await Promise.all([
  this.userMetaRepository.findUserEntityById(curatorId),
  this.promoterAssociationRepository.findPostByAliasAndCreatorId({
    urlAlias: postAlias,
    creatorId: curatorId,
    allowPromotersResell: true,  // Post 必须允许转售
  }),
  this.userMetaRepository.findAffiliateOrVerifiedUserByPhoneNumberOrEmail(phoneNumberOrEmail),
]);

3. 业务规则验证

规则 说明 错误码
Curator 必须存在 查询用户表 404
Post 必须存在且允许转售 allowPromotersResell = true 404
不能为 Post 所有者创建 curator.userId !== promoter.userId 400
防止重复转售 检查 post.uplineCreatorList 400
不能为商品所有者创建 检查 post.relatedProducts 400

4. Promoter 用户创建

如果 Promoter 不存在,自动创建新用户:

属性
userRole CONSUMER
type GUEST
createdFeature AFFILIATE
email / phoneNumber 来自请求参数

5. PromoterAssociation 创建/更新

await this.promoterAssociationService.createPromoterAssociation({
  promoterId: promoterUserEntity.userId,
  curatorId,
  note,
});

服务层会:

  • 查找 Promoter 最新的 POST 或 MIXED 类型的 StoreFrontModule
  • 创建新的 PromoterAssociation 或更新已删除的关联
  • 发送 AddPostAccessPromoter 事件(用于受限 Post 的访问权限)

关键数据模型 | Data Models

PromoterAssociation

model PromoterAssociation {
  id                       String                      @id
  promoterId               String
  curatorId                String
  invitationType           InvitationType             @default(AFFILIATE)
  invitationCode           String?
  allowSyncPosts           Boolean                    @default(true)
  syncStoreFrontModuleIds  String[]
  note                     String?
  deletedAt                DateTime?
  createdAt                DateTime                    @default(now())
  updatedAt                DateTime                    @updatedAt

  // Relations
  promoter                 User                        @relation("PromoterAssociations_PromoterId")
  curator                  User                        @relation("PromoterAssociations_CuratorId")

  @@unique([promoterId, curatorId])
}

InvitationType

enum InvitationType {
  INVITATION_LINK  // 标准推广链接
  AFFILIATE       // 联盟推广链接
}

注意事项 | Important Notes

[!danger] 不可逆操作

  • 创建 PromoterAssociation 后会发送 AddPostAccessPromoter 事件
  • 受限 Post (isAccessRestricted = true) 会自动添加 Promoter 到访问列表
  • 需通过断开连接接口才能撤销关联

[!warning] 用户类型升级

  • 新创建的用户初始为 role=CONSUMER, type=GUEST
  • 当有订单产生时,会自动升级为 role=PROMOTER, type=AFFILIATE (见 updateRoleAndTypeFromOrder)

[!tip] StoreFront 同步

  • allowSyncPosts 默认为 true
  • syncStoreFrontModuleIds 自动设置为 Promoter 最新的 POST/MIXED 模块

[!info] 相关接口

  • POST /promoter-association/guest/affiliate-link - 公开接口,无需认证
  • GET /promoter-association/curators - 获取关联的 Curator 列表
  • DELETE /promoter-association/curator/:curatorId - 断开连接

测试用例 | Test Cases

成功场景

场景 预期结果
为新用户(不存在)创建链接 创建新用户 + PromoterAssociation,返回 200
为已验证用户创建链接 创建 PromoterAssociation,返回 200
添加 note 参数 note 保存到 PromoterAssociation

失败场景

场景 预期结果
curatorId 与当前用户不一致 400 权限错误
Post 不存在 404
Post 不允许转售 404
为 Post 所有者创建链接 400
Promoter 已在 Post 的转售链中 400
Promoter 是 Post 的商品所有者 400
phoneNumberOrEmail 格式无效 400

文件路径 说明
src/promoter-association/promoter-association.controller.ts 控制器 (第 298-384 行)
src/promoter-association/promoter-association.service.ts 服务层 (第 51-83 行)
src/promoter-association/promoter-association.repo.ts 数据访问层 (第 28-99 行)
src/promoter-association/promoter-association.interface.ts DTO 定义 (第 16-58 行)

变更历史 | Changelog

版本 日期 变更内容
1.0.0 2026-02-26 初始版本,支持创建联盟推广链接

  • [[KAT-10550]] - View all affiliate post links
  • [[KAT-6637]] - Affiliate users should not show in Collaboration

results matching ""

    No results matching ""